feat(web): wire Studio Act on findings to durable video-to-actions (WDK v1) - #1507
Conversation
…DK v1) Product path for Option B: start/poll Workflow DevKit runs from Studio, harden steps to call transcription + action-agent libs directly (no self-HTTP loopback), and document the dual pipeline vs FastAPI SSE path. Closes #1506
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| '/api/training', | ||
| '/api/transcribe', | ||
| '/api/video', | ||
| '/api/workflows', |
There was a problem hiding this comment.
Confirmed against the branch head (afa703b) — this is a real defect, and the numbers make it deterministic rather than a corner case.
The arithmetic
proxy.ts:30—AI_LIMIT = 12perWINDOW_SECONDS = 60.proxy.ts:124-125—isAiRouteis a pure prefix match on the pathname; it never looks atrequest.method. So/api/workflowscovers thePOSTstart and theGET /api/workflows/video-to-actions/:runIdstatus endpoint identically.studio-workflow.ts:140-141— the poller defaults toattempts = 20,delayMs = 1500, i.e. one request every 1.5s ≈ 40 req/min sustained.
40 req/min against a 12/min bucket means the poller exhausts the window at roughly poll #12, ~18s in — and it is configured to keep going to poll #20 (~30s). Polls 13–20 are 429s on every run. The POST start draws from the same bucket, so it's actually poll #11.
Why it will read as a hang rather than an error
pollVideoToActions falls through to Still ${last.runStatus || 'running'} after ${attempts} polls (studio-workflow.ts:167). A 429 that isn't distinguished from "not terminal yet" lands in exactly that branch, so the UI reports still running for a request that was refused. The PR's own stated fallback behaviour masks the failure — which is what makes this worth fixing before merge rather than after.
Suggested shape
Splitting on method is the smallest change that keeps the intent: the expensive thing here is starting a run, not reading its status. Either gate the AI limit on non-idempotent methods, or list the start path explicitly (/api/workflows/video-to-actions for POST) and let the status GET fall through to GENERAL_LIMIT.
Worth noting GENERAL_LIMIT defaults to 60/min, so a 40 req/min poller only has ~33% headroom there — a second concurrent run in the same tab would breach it. Raising delayMs to ~2500ms (24 req/min) alongside the method split would leave the poll cadence comfortably inside the general bucket without changing the ~30s wall-clock budget materially.
Not pushing a fix here: this branch isn't mine to write to, and the change is small enough that it belongs in the author's next commit.
Generated by Claude Code
There was a problem hiding this comment.
Thanks for confirming — agreed the numbers make this deterministic. The suggestion I attached takes the method-split approach you outline (only the POST start stays AI-classified; the GET status poll falls through to GENERAL_LIMIT).
Your headroom point is worth acting on too: at GENERAL_LIMIT=60/min, the default poller (attempts:20 delayMs:1500 ≈ 40/min) leaves only ~33% margin, so a second concurrent run in the same tab/IP would breach it. Raising delayMs to ~2500ms (≈24/min) as a companion change keeps the poll cadence comfortably inside the general bucket without materially changing the ~30s wall-clock budget. I'd treat the method-split as the fix and the interval bump as defense-in-depth.
There was a problem hiding this comment.
Addressed in follow-up #1524 — /api/workflows is now classified by request method, so the GET status poll uses the 60/min general budget while only the POST start keeps the 12/min AI budget; added a regression test asserting the poll's X-RateLimit-Limit is 60. Not marking this thread resolved since #1507 already merged.
🤖 Addressed by Claude Code
Red-team pass — this PR has had no review at allFlagging first: CodeRabbit reported Three findings. One I'd treat as blocking. 1. The new edge SSRF check blocks almost nothing — and reintroduces the exact bracket bug #1486 is fixing
host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' ||
host === '::1' || host.endsWith('.local') || host.endsWith('.internal')Measured by running that predicate over
The The fix is to not write this check. Severity, stated accurately: both routes are session-gated — 2. The status route leaks internal error detail to the client (CWE-209)return NextResponse.json({
ok: false, runId,
error: message, // raw err.message from the workflow world
hint: 'Ensure the workflow package is installed and withWorkflow wraps next.config.',
}, { status: 500 });This is the class the repo just spent #1381 and #1428 closing — collapsing client-visible rejection detail onto a constant and moving the cause to the server logs. The 3. Minor: uncleared timer in the
|
Review — scheduled remediation routine, head
|
Confirming the rate-limit finding, with the numbers — and it's wider than the status pollerAutomated PR-remediation sweep. The Vercel Agent review thread on 1. The poller exceeds the budget by ~3.3× on its own
function isAiRoute(pathname: string): boolean {
return AI_ROUTE_PREFIXES.some((prefix) => pathname.startsWith(prefix));
}
function getRateLimit(pathname: string): number {
return isAiRoute(pathname) ? AI_LIMIT : GENERAL_LIMIT;
}So Against that, The 12th request is the one that 429s, which lands roughly 17 seconds into a 30-second poll window — before the run has any realistic chance of finishing, since the workflow does a transcript fetch plus an action-agent call. The UI reaches its 2. The blast radius is every other AI route, not just this oneThis is the part worth flagging beyond the original comment. The bucket key is class-scoped, not path-scoped: const routeClass = isAiRoute(pathname) ? 'ai' : 'api';
const key = `${routeClass}:${clientIp}`;So all Suggested fix — make the AI class method-awareThe // Status polls are cheap reads that do no model work, but they share the
// `ai:<ip>` bucket with /api/chat and /api/transcribe. One Studio run polls
// 20x at 1.5s, which exhausts a 12/min budget in ~17s and takes the other
// AI routes down with it. Only the mutating call is AI-class.
const AI_ROUTE_METHOD_EXEMPT: Record<string, ReadonlySet<string>> = {
'/api/workflows': new Set(['GET', 'HEAD']),
};
function isAiRoute(pathname: string, method: string): boolean {
return AI_ROUTE_PREFIXES.some(
(prefix) =>
pathname.startsWith(prefix) && !AI_ROUTE_METHOD_EXEMPT[prefix]?.has(method),
);
}with Worth a test pinning it, since nothing currently asserts the classification: Cheaper alternative if you'd rather not touch the classifier: raise Terminal state
On CI: the Actions queue is deep (251 runs queued, 22 in progress at the time of writing) and latency from run creation to conclusion is running ~20 minutes, so checks on this head should conclude rather than hang. Contrary to a note left on #1486, runs are concluding — 29 of the last 30 Generated by Claude Code |
Red-team pass on the diff (PR-remediation sweep)CI is still fully queued on The workflow refactor is a clear improvement: dropping the 1. The edge SSRF check allows every RFC1918 literal and the metadata address — measured
Two separate problems. The list omits 10/8, 172.16/12, 192.168/16, 169.254/16 and CGNAT entirely. And
import { assertPublicHttpUrl } from '@/lib/ssrf-guard';
// ...
try {
await assertPublicHttpUrl(url);
} catch {
return NextResponse.json({ error: 'url host is not allowed' }, { status: 400 });
}Note the bare Severity is bounded, deliberately. The comment is right that 2.
|
Finding 1 fixed and pushed —
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
🔍 PR Validation |
…t bucket (#1518) * fix(web): stop workflow status polls draining the shared AI rate-limit bucket #1507 added /api/workflows to AI_ROUTE_PREFIXES. isAiRoute classified by path prefix alone, so the polled GET status endpoint was metered against the AI budget (default 12/min) while pollVideoToActions polled at 40/min. The 12th request 429'd ~17s into a 30s window, before a transcript fetch plus an agent call could finish. The bucket is keyed by class, not path, so every AI prefix shares one ai:<ip> counter -- a single Studio run also 429'd /api/chat, /api/transcribe and /api/pipeline as collateral. Move the classifier into auth-paths.ts, which exists as the home for path policy free of Next.js request types so vitest can import it offline, and make it method-aware. GET/HEAD on /api/workflows falls to the general budget; POST stays AI-class because starting a run does real model work. The exemption is keyed per-prefix rather than exempting GET globally, so it cannot widen another route that later serves model work over GET. An omitted method defaults to POST so the failure mode is the stricter limit. Also retune the poller to 30 attempts x 2s: 30 req/min leaves roughly half the general allowance for the rest of the page, and the wall-clock window doubles to 60s, which better fits the work the run actually does. Closes #1517 * fix(web): require a segment boundary before exempting a route from the AI budget Self-review follow-up. The exemption looked the prefix up with the same loose startsWith used for class membership, so a future sibling surface whose name merely starts with an exempted prefix -- /api/workflows-admin -- would silently inherit the GET carve-out and drop onto the looser budget. No route in the tree does this today (checked every directory under apps/web/src/app/api), so this is latent rather than live. It is worth closing while the file is open: the same shape, an incidental block quietly becoming an allow, is what #1486 had to fix in the SSRF guard. Class membership keeps its original loose matching. Narrowing that would move routes off the stricter budget, which this change has no business doing; the exemption is the widening, so only the exemption is tightened. --------- Co-authored-by: Claude <noreply@anthropic.com>
Canonical issue
Closes #1506
Outcome
Studio can Act on findings via a durable Workflow DevKit run: video URL → transcript → action agent, with
runIdstart + status poll. Complements the existing FastAPI/api/pipeline+ SSE path (still used for Deploy / Dashboard).Scope
video-to-actionssteps to callfetchTranscript/runActionAgentdirectly (no self-HTTP loopback)POST /api/workflows/video-to-actionsedge host checks +statusUrlGET /api/workflows/video-to-actions/:runIdviagetRunstudio-workflow.ts+ unit tests/api/workflowsdocs/WORKFLOW_DEVKIT.mdProduct v1Risk
start()fails if workflow world misconfigured; Studio falls back to messaging + existing Dashboard/Deploy pathsgit revert; Studio button becomes a no-op path if API 500sVerification
npx vitest run src/lib/__tests__/studio-workflow.test.ts src/lib/__tests__/studio-deploy.test.ts— 7 passedtest-frontendCI on PRProduction evidence
Vercel preview on this branch. Runtime evidence requires provider keys + a real YouTube URL on preview; unit tests cover client start/poll contract.
Agent handoff
Land after CI green. Next product step: Option C (durable Studio deploy kickoff/poll) or stream step progress via
getWritable().